<?php
/**
 * 框架 - 简单的[ActiveRecord]模式
 * 
 * @version $Id: ActiveRecord.php 436 2014-06-10 08:46:21Z wenjingpeng7@gmail.com $
 * @package Mysql
 */
class ActiveRecord
{
	/**
	 * @var 数据库表名,若没配置则可使用{@setTable}方法进行配置
	 * // 没有配置些属性,在使用过程序系统会抛出异常
	 */
	public $table;
	
	/**
	 * @var 连接数据源序号(当有多个数据源时，需要配置)
	 * // 系统会自动从"main.dsn"中加载相应的配置
	 */
	public $dsnNumber = 0;
	
	/**
	 * @var [分表]是否进行分表存在性验证和创建操作
	 * // 此属性的意义在于若分表都在存后则不必在进行检查操作(可以提高性能)
	 */
	public $needCheckCutTable = true;
	
	/**
	 * @var [分表]对应的创建分表的SQL
	 * // 将数据表名使用%s替换,系统在初始数据表名后会进行填充
	 */
	public $sqlPartition = null;
	
	/**
	 * @var [分表]配置信息
	 * 
	 * @example array('type'=>'callback', 'callback'=>callback)
	 * @example array('type'=>'mod2', 'num'=>200)
	 * @example array('type'=>'mod', 'num'=>200)
	 * @example array('type'=>'', 'num'=>200)
	 */
	public $confPartition = array();
	
	
	/// 以上属性可根据场景进行设置 ///
	
	
	/**
	 * @var 数据查询对象
	 */
	public $oSelect;
	
	/**
	 * @var 数据操作对象
	 */
	public $oCommand;
	
	/**
	 * @var 数据操作或查询使用的条件对象
	 */
	public $oCondition;
	
	/**
	 * @var `数据库表名`
	 */
	public $safeTable;
	
	/**
	 * @var [分表]是否已检查表的存在性
	 */
	private $_checkCutTable = false;
	
	/**
	 * 构造函数
	 * 
	 * @param mixed $fieldId 用于数据分表的字段值(单个或组合)
	 */
	public function __construct($fieldId = null)
	{
		$this->_initTable($fieldId);
		
		///
		$this->oCondition = new ArCondition($this->safeTable);
		$this->oCommand = new ArCommand($this->safeTable, $this->oCondition);
		$this->oSelect = new ArSelect($this->safeTable, $this->oCondition);
	}
	
	/**
	 * 初始化数据表名(包含:前缀,后缀)
	 * 
	 * @param mixed $fieldId 用于数据分表的字段值(单个或组合)
	 */
	private function _initTable($fieldId)
	{
		if ( ! empty($this->table)) {
			///
			$key = 'main.dbTable.'. $this->dsnNumber;
			$conf = App::O($key);
			if (isset($conf['prefix'])) 
				$this->table = $conf['prefix'] . $this->table;
				
			///
			$o = $this->confPartition;
			if ( ! empty($fieldId) && ! empty($o)) {
				if ($o['type'] == 'callback') {
					if ( ! is_callable($o['callback'])) 
						throw new Exception("[ActiveRecord] 分表回调方法有误");
						
					$suffix = call_user_func_array($o['callback'], array($fieldId));
				} else {
					$baseNum = intval($o['num']);
					if (empty($baseNum)) 
						throw new Exception("[ActiveRecord] 分表基数有误");
						
					switch ($o['type']) {
						case 'mod':
							$suffix = ($fieldId%$baseNum)+1;
							break;
						case 'mod2':
							$suffix = ($fieldId%$baseNum);
							break;
						default:
							$suffix = ceil($fieldId/$baseNum);
							break;
					}
				}
				$this->table .= ((isset($o['suffixSpace'])?$o['suffixSpace']:'_') . $suffix);
			}
			
			///
			$this->safeTable = lpString::dbQuote($this->table);
		}
	}
	
	/**
	 * 构造查询条件
	 * 
	 * @param string $by 字段名
	 * @param string $var 字段值
	 */
	private function _makeCondition($by, $var)
	{
		if (is_array($by) && is_array($var)) {
			if (count($by) == count($var)) {
				$data = array();
				foreach ($by as $i => $f) 
					$data[$f] = $var[$i];
			}
		} elseif ( ! is_array($by) && ! is_array($var)) 
			$data = array($by=>$var);
			
		///
		if (isset($data)) 
			$this->where($data);
	}
	
	/**
	 * 取得DBO,同时判断分表的状态
	 * 
	 * @return LpMysql
	 */
	public function getDbo()
	{
		$dbo = App::getMysql($this->dsnNumber);
		
		///
		$flag = $this->needCheckCutTable && ! $this->_checkCutTable;
		if ($flag && $this->table && $this->sqlPartition) {
			$this->_checkCutTable = true;
			if ( ! $dbo->existsTable($this->table)) 
				$dbo->operate(sprintf($this->sqlPartition, $this->table));
		}
		return $dbo;
	}
	
	/**
	 * 返回查询SQL
	 * 
	 * @return string
	 */
	public function getSql()
	{
		if (empty($this->table)) 
			throw new Exception("[ActiveRecord] 模型数据表名未设置");
			
		///
		return $this->oSelect->__toString();
	}
	
	/**
	 * 插入数据,成功后若数据表有自增长字段则会返回,失败则返回false
	 * 
	 * @param array $data 待插入的数据
	 * @param array $fields 字段列表
	 * @param bool $affected 返回受影响行数
	 * @param bool $ignore 使用IGNORE参数
	 * @return mixed
	 */
	public function insert($data, $fields = array(), $affected = false, $ignore = false)
	{
		$sql = $this->oCommand->insert($data, $fields, $ignore);
		
		///
		return $this->execute($sql, $affected, true);
	}
	
	/**
	 * 插入数据,若主键存在则执行“ON DUPLICATE KEY UPDATE”
	 * 
	 * @param array $data 待插入的数据
	 * @param array $fields 字段列表
	 * @param array $duplicateFields 需要执行更新字段
	 * @return mixed
	 */
	public function insertDuplicate($data, $fields = array(), $duplicateFields = array(), $affected = false)
	{
		$sql = $this->oCommand->insertDuplicate($data, $fields, $duplicateFields);
		///
		return $this->execute($sql, $affected, true);
	}
	
	/**
	 * 删除数据,需要先调用where再调用此操作进行删除
	 * 
	 * @return bool
	 */
	public function delete($affected = true)
	{
		$sql = $this->oCommand->delete();
		///
		return $this->execute($sql, $affected, true);
	}
	
	/**
	 * 删除数据,根据某个字段
	 * 
	 * @param string $by 字段名
	 * @param string $var 字段值
	 * @return bool
	 */
	public function deleteBy($by, $var, $affected = true)
	{
		$this->_makeCondition($by, $var);
		return $this->delete($affected);
	}
	
	/**
	 * 删除数据,根据主键
	 * 
	 * @param string $id 主键值
	 * @return bool
	 */
	public function deleteById($id, $affected = true)
	{
		return $this->deleteBy('id', $id, $affected);
	}
	
	/**
	 * 更新数据,需要先调用where再调用此操作进行更新
	 * 
	 * @param array $data 待更新的数据
	 * @param array $fields 字段列表
	 * @return bool
	 */
	public function update($data, $fields = array(), $affected = false)
	{
		$sql = $this->oCommand->update($data, $fields);
		///
		return $this->execute($sql, $affected, true);
	}
	
	/**
	 * 更新数据,根据某个字段
	 * 
	 * @param string $by 字段名
	 * @param string $var 字段值
	 * @param array $data 数据源
	 * @param array $fields 字段列表
	 * @return bool
	 */
	public function updateBy($by, $var, $data, $fields = array(), $affected = false)
	{
		$this->_makeCondition($by, $var);
		return $this->update($data, $fields, $affected);
	}
	
	/**
	 * 更新数据,根据主键
	 * 
	 * @param string $id 主键值
	 * @param array $data 数据源
	 * @param array $fields 字段列表
	 * @return bool
	 */
	public function updateById($id, $data, $fields = array(), $affected = false)
	{
		return $this->updateBy('id', $id, $data, $fields, $affected);
	}
	
	/**
	 * 在某个字段上进行递增或递减操作
	 * 
	 * @param string $field 待更新的字段
	 * @param bool $plus true为递增,false为递减
	 * @param int $step 步长
	 * @return bool
	 */
	public function updateStep($field, $plus = true, $step = 1, $affected = false)
	{
		$sql = $this->oCommand->updateStep($field, $plus, $step);
		///
		return $this->execute($sql, $affected, true);
	}
	
	/**
	 * 执行数据操作SQL
	 * 
	 * @param string $sql 待执行的SQL
	 * @param bool $affected 是否返回影响行数
	 * @param bool $checkTable 是否验证表模型
	 * @return mixed
	 */
	function execute($sql, $affected = false, $checkTable = false)
	{
		if ($checkTable && empty($this->table)) 
			throw new Exception("[ActiveRecord] 模型数据表名未设置");
			
		///
		return $this->getDbo()->query($sql, $affected);
	}
	
	/**
	 * [Select]设置重复
	 * 
	 * @return $this
	 */
	public function distinct()
	{
		$this->oSelect->set('DISTINCT', true);
		return $this;
	}
	
	/**
	 * [Select]设置操作条件
	 * @example where('id = ? AND uid = ?', 1, 2)
	 * 
	 * @return $this
	 */
	public function where()
	{
		$args = func_get_args();
		$this->oCondition->add($args, true);
		return $this;
	}
	
	/**
	 * [Select]设置操作条件
	 * @example where('uid = ?', 1)
	 * 
	 * @return $this
	 */
	public function orWhere()
	{
		$args = func_get_args();
		$this->oCondition->add($args, false);
		return $this;
	}
	
	/**
	 * 子查询
	 */
	public function fromSub($ar)
	{
		$this->oSelect->fromSub($ar->oSelect);
		return $this;
	}
	
	/**
	 * [Select]设置聚合
	 * @example group('date')
	 * 
	 * @param string $field 聚合的字段
	 * @return $this
	 */
	public function group($field)
	{
		$this->oSelect->set('GROUP', $field);
		return $this;
	}
	
	/**
	 * [Select]设置查询区间
	 * @example limit(0, 1)
	 * 
	 * @param int $start 开始
	 * @param int $offset 区间
	 * @return $this
	 */
	public function limit($start, $offset)
	{
		$this->oSelect->set('LIMIT', array($start, $offset));
		return $this;
	}
	
	/**
	 * [Select]设置查询字段
	 * @example field('id,title')
	 * 
	 * @param string $field 查询的字段
	 * @return $this
	 */
	public function field($field)
	{
		$this->oSelect->push('FIELD', $field);
		return $this;
	}
	
	/**
	 * [Select]设置查询排序
	 * @example order('`field` DESC')
	 * 
	 * @param string $field 排序的字段
	 * @param int $index 序号(用于定义排序顺序)
	 * @param int $preTable 构造SQL时，是否添加表前缀(聚合查询时不需要)
	 * @return $this
	 */
	public function order($field, $index = 0, $preTable = true)
	{
		$this->oSelect->push('ORDER', array($field, $index, $preTable));
		return $this;
	}
	
	/**
	 * 聚合查询 - 取总数
	 * 
	 * @param string $field 字段
	 * @return string
	 */
	public function getCount($field = '')
	{
		$this->oSelect->setAggregation('COUNT', $field);
		return $this->getVar();
	}
	
	/**
	 * 聚合查询 - 取最大值
	 * 
	 * @param string $field 字段
	 * @return string
	 */
	public function getMax($field = '')
	{
		$this->oSelect->setAggregation('MAX', $field);
		return $this->getVar();
	}
	
	/**
	 * 聚合查询 - 取最小值
	 * 
	 * @param string $field 字段
	 * @return string
	 */
	public function getMin($field = '')
	{
		$this->oSelect->setAggregation('MIN', $field);
		return $this->getVar();
	}
	
	/**
	 * 聚合查询 - 取平均值
	 * 
	 * @param string $field 字段
	 * @return string
	 */
	public function getAvg($field = '')
	{
		$this->oSelect->setAggregation('AVG', $field);
		return $this->getVar();
	}
	
	/**
	 * 聚合查询 - 取和
	 * 
	 * @param string $field 字段
	 * @return string
	 */
	public function getSum($field = '')
	{
		$this->oSelect->setAggregation('SUM', $field);
		return $this->getVar();
	}
	
	/**
	 * 存在性检测
	 * 
	 * @param string $by 字段名
	 * @param string $var 字段值
	 * @return bool
	 */
	public function existsBy($by, $var)
	{
		$this->_makeCondition($by, $var);
		return $this->getCount() > 0;
	}
	
	/**
	 * 存在性检测
	 * 
	 * @param string $id 主键值
	 * @return bool
	 */
	public function existsById($id)
	{
		return $this->existsBy('id', $id);
	}
	
	/**
	 * 取得单个数据
	 * 
	 * @param string $by 字段名
	 * @param string $var 字段值
	 * @param string $field 字段
	 * @return mixed
	 */
	public function getBy($by, $var, $field = '')
	{
		$this->_makeCondition($by, $var);
		
		///
		if ( ! empty($field)) 
			return $this->field($field)->getVar();
		else 
			return $this->getRow();
	}
	
	/**
	 * 取得单个数据
	 * 
	 * @param string $id 主键值
	 * @param string $field 字段
	 * @return mixed
	 */
	public function getById($id, $field = '')
	{
		return $this->getBy('id', $id, $field);
	}
	
	/**
	 * 取得单行列记录值
	 * 
	 * @param string $sql 查询SQL
	 * @return minx
	 */
	public function getVar($sql = '')
	{
		$sql or $sql = $this->getSql();
		///
		return $this->getDbo()->getVar($sql);
	}
	
	/**
	 * 取得某列记录
	 * 
	 * @param string $sql 查询SQL
	 * @return minx
	 */
	public function getCol($sql = '')
	{
		$sql or $sql = $this->getSql();
		///
		return $this->getDbo()->getCol($sql);
	}
	
	/**
	 * 取得单行记录
	 * 
	 * @param string $sql 查询SQL
	 * @return minx
	 */
	public function getRow($sql = '')
	{
		if (empty($sql)) {
			if ( ! $this->oSelect->hadSet('LIMIT')) 
				$this->limit(0, 1);
				
			$sql = $this->getSql();
		}
		///
		return $this->getDbo()->getRow($sql);
	}
	
	/**
	 * 取得多行记录
	 * 
	 * @param string $sql 查询SQL
	 * @return minx
	 */
	public function getResults($sql = '')
	{
		$sql or $sql = $this->getSql();
		///
		return $this->getDbo()->getResults($sql);
	}
	
	/**
	 * 取得模型对应的表名,封装着数据表的前/后缀
	 * 
	 * @param bool $safety 是否添加"``"引用
	 * @return string
	 */
	public function getTable($safety = false)
	{
		return $safety ? $this->safeTable : $this->table;
	}
	
	
	/// 以下两个在考虑是否去除(在使用过程需注意) ///
	
	
	/**
	 * 设置数据表名,提供于在模型子类中没有进行表配置的
	 * 
	 * @param string $table 数据表
	 * @param mixed $fieldId 用于数据分表的字段值(单个或组合)
	 */
	public function setTable($table, $fieldId = null)
	{
		$this->table = $table;
		$this->_initTable($fieldId);
		$this->oSelect->setTable($this->safeTable);
		$this->oCommand->setTable($this->safeTable);
		$this->oCondition->setTable($this->safeTable);
	}
	
	/**
	 * 设置数据源序号
	 * // 用于在同一个模型中操作到不同的数据源
	 * // 注意在切换后是否需要再切换回来
	 * 
	 * @param int $num 连接数据源序号
	 */
	public function setDsnNumber($num = 0)
	{
		$this->dsnNumber = $num;
	}
}
